﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml.Linq;

using LuaInterface;
using Jint;

using log4net;

namespace MND
{
    class Script : ICloneable
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(Script));

        private Language language;
        private Dictionary<string, InstanceMethod> bindedFunctions = new Dictionary<string, InstanceMethod>();
        private Dictionary<string, object> bindedVars = new Dictionary<string, object>();
        private Dictionary<string, ScriptReturn> returns = new Dictionary<string, ScriptReturn>();

        private string[] script;

        private bool isReady = false;
        
        private int currentLine = -1;

        private Lua lua;
        private JintEngine jint;

        public Script(string xmlFileName)
            : this(
                new System.IO.MemoryStream(
                Encoding.UTF8.GetBytes(
                System.IO.File.ReadAllText(xmlFileName)
                ))) { }
        public Script(System.IO.Stream stream) : this(XDocument.Load(stream).Root) { }
        public Script(XElement xDoc)
        {
            try
            {
                // === LANGUAGE ===
                XElement xLang = (from lang in xDoc.Descendants("Language")
                                  select lang).First();
                language = (Language)Enum.Parse(typeof(Language), xLang.Value, true);


                // === BINDINGS ===
                IEnumerable<XElement> iexBindings = (from binding in xDoc.Descendants("Bindings")
                                                     select binding);


                // === FUNCTIONS ===
                foreach (XElement xBindFunction in iexBindings.Descendants("Function"))
                {
                    string fName = xBindFunction.Value;

                    bindedFunctions.Add(fName, null);
                }


                // === VARIABLES ===
                foreach (XElement xBindVar in iexBindings.Descendants("Var"))
                {
                    string fName = xBindVar.Value;

                    bindedVars.Add(fName, null);
                }

                // === RETURNS ===
                IEnumerable<XElement> iexReturns = (from v in xDoc.Descendants("Returns")
                                                    select v);
                foreach (XElement xVar in iexReturns.Descendants("Var"))
                {
                    string varName = xVar.Descendants("Name").First().Value;

                    IEnumerable<XElement> xValues = xVar.Descendants("Values");
                    if (xValues.Count() > 0)
                    {
                        List<string> valEnum = new List<string>();
                        foreach (XElement xVal in xValues)
                        {
                            valEnum.Add(xVal.Descendants("Code").First().Value);
                        }
                        returns.Add(varName, new ScriptReturn(valEnum.ToArray()));
                    }
                    else
                    {
                        returns.Add(varName, new ScriptReturn());
                    }
                }


                // === SCRIPT ===
                XElement xScript = (from steps in xDoc.Descendants("Script")
                                    select steps).First();
                /*
                script = new string[xScript.Descendants("Step").Count()];
                foreach (XElement step in xScript.Descendants("Step"))
                {
                    int stepNumber = int.Parse(step.Attribute("id").Value);
                    script[stepNumber] = step.Value;
                }
                 */
                string s = xScript.Value;
                //script = s.Split(new char[] { '\r', '\n' });
                script = s.Split(new string[] { "@@" }, StringSplitOptions.RemoveEmptyEntries);
            }
            catch (Exception e)
            {
                log.Error("Error while creating script.", e);
                throw new ArgumentException("Given xml file is not parsing as proper Script", e);
            }
        }

        public void BindMethod(MethodBase method, object instance)
        {
            BindMethod(method, instance, method.Name);
        }
        public void BindMethod(MethodBase method, object instance, string functionName)
        {
            if (language == Language.JS)
                throw new Exception("JS doesn't support dynamic function binding");
            if (bindedFunctions.ContainsKey(functionName))
            {
                if (bindedFunctions[functionName] == null)
                    bindedFunctions[functionName] = new InstanceMethod(instance, method);
                else throw new ArgumentException(String.Format("Function {0} is already binded.", functionName));
            }
        }
        public void BindVar(string name, object value)
        {
            if (bindedVars.ContainsKey(name))
            {
                if (bindedVars[name] == null)
                    bindedVars[name] = value;
                else throw new ArgumentException(String.Format("Variable {0} is already binded", name));
            }
        }

        public bool IsBinded
        {
            get
            {
                // Funcs
                foreach (string key in bindedFunctions.Keys)
                {
                    if (bindedFunctions[key] == null)
                        return false;
                }

                // Vars
                foreach (string key in bindedVars.Keys)
                {
                    if (bindedVars[key] == null)
                        return false;
                }
                return true;
            }
        }

        private void Prepare()
        {
            if (IsBinded)
            {
                switch (language)
                {
                    case Language.Lua:
                        lua = new Lua();

                        // Methods
                        foreach (KeyValuePair<string, InstanceMethod> kvp in bindedFunctions)
                        {
                            lua.RegisterFunction(kvp.Key, kvp.Value.Instance, kvp.Value.Method);
                        }
                        lua.RegisterFunction("SetReturn", this, this.GetType().GetMethod("SetReturn"));

                        // Vars
                        foreach (string key in bindedVars.Keys)
                        {
                            lua[key] = bindedVars[key];
                        }

                        isReady = true;
                        break;
                    case Language.JS:
                        jint = new JintEngine();

                        // Methods
                        foreach (KeyValuePair<string, InstanceMethod> kvp in bindedFunctions)
                        {
                            jint.SetFunction(kvp.Key, kvp.Value.Delegate);
                        }
                        jint.SetFunction("SetReturn", new Action<string,string>((n,v) => SetReturn(n,v)));

                        // Vars
                        foreach (string key in bindedVars.Keys)
                        {
                            jint.SetParameter(key, bindedVars[key]);
                        }

                        isReady = true;
                        break;
                }
            }
            else
            {
                List<string> funcs = new List<string>();
                List<string> vars = new List<string>();
                foreach (string key in bindedFunctions.Keys)
                {
                    if (bindedFunctions[key] == null)
                        funcs.Add(key);
                }

                // Vars
                foreach (string key in bindedVars.Keys)
                {
                    if (bindedVars[key] == null)
                        vars.Add(key);
                }

                throw new BindingException(String.Format("Not all funcs/vars are binded. Funcs:[{0}] Vars:[{1}]", String.Join(",", funcs.ToArray()), String.Join(",", vars.ToArray())));
            }
        }

        #region Returns
        public void SetReturn(string name, string value)
        {
            if (returns.ContainsKey(name))
                returns[name].Set(value);
            else
                throw new ArgumentException("Trying to set undefined return variable: " + name);
        }

        public string GetReturn(string name)
        {
            if (returns.ContainsKey(name))
                return returns[name].Get();
            else throw new ArgumentException("Argument not found:" + name);
        }
        #endregion

        public void SetString(string name, string value)
        {
            switch (language)
            {
                case Language.JS:
                    jint.SetParameter(name, value);
                    break;
                case Language.Lua:
                    lua[name] = value;
                    break;
            }
        }

        #region Execute
        public bool ExecuteNext(){
            return Execute(++currentLine);
        }

        private bool Execute(int p)
        {
            if (!isReady)
                Prepare();

            if (p < script.Length)
            {
                switch (language)
                {
                    case Language.Lua:
                        lua.DoString(script[p]);
                        break;
                    case Language.JS:
                        jint.Run(script[p]);
                        break;
                }
                return true;
            }
            else return false;
        }
        #endregion
        internal enum Language { Lua, JS }
        internal class InstanceMethod
        {
            public object Instance;
            public MethodBase Method;

            // JS
            public Delegate Delegate;

            public InstanceMethod(object instance, MethodBase methodBase)
            {
                Instance = instance;
                Method = methodBase;
            }

            public InstanceMethod(Delegate d)
            {
                this.Delegate = d;
            }
        }
        internal class ScriptReturn
        {
            private ReturnType type;
            private string[] returnValues;
            private string returnValue;

            public ScriptReturn(string[] values)
            {
                type = ReturnType.ENUM;
                returnValues = values;
            }
            public ScriptReturn()
            {
                type = ReturnType.String;
            }

            public string Get()
            {
                return returnValue;
            }

            public void Set(string value)
            {
                switch (type)
                {
                    case ReturnType.ENUM:
                        if (returnValues.Contains(value))
                            returnValue = value;
                        else
                            throw new ArgumentException("Cannot specify argument out of declared range");
                        break;
                    case ReturnType.String:
                        returnValue = value;
                        break;
                }
            }
            internal enum ReturnType { String, ENUM }
        }
        internal class BindingException : Exception
        {
            public BindingException(string message) : base(message) { }
        }

        private Script(Language pLanguage, Dictionary<string, InstanceMethod> pBindedFunctions, Dictionary<string, object> pBindedVars, Dictionary<string, ScriptReturn> pReturns, string[] pScript)
        {
            this.language = pLanguage;
            this.bindedFunctions = new Dictionary<string, InstanceMethod>(pBindedFunctions);
            this.bindedVars = new Dictionary<string,object>(pBindedVars);
            this.returns = new Dictionary<string,ScriptReturn>(pReturns);
            this.script = pScript;

            switch (language)
            {
                case Language.Lua:
                    this.lua = new Lua();
                    break;
                case Language.JS:
                    this.jint = new JintEngine();
                    break;
            }
        }
        public object Clone()
        {
            return new Script(language, bindedFunctions, bindedVars, returns, script);
        }
    }
}
